﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

namespace Celeriq.Utilities
{
	/// <summary>
	/// This class provides a way to cache and retrieve serializable items to disk
	/// </summary>
	/// <typeparam name="T"></typeparam>
	public class FileCacheHelper<T> : System.IDisposable
	{
		private object locker = new object();
		private long _lastPosition = 0;

		public FileCacheHelper()
		{
			this.CacheFileName = Path.GetTempFileName();
		}

		public FileCacheHelper(string cacheFile)
			: this()
		{
			if (File.Exists(this.CacheFileName))
				File.Delete(this.CacheFileName);

			if (!File.Exists(cacheFile))
				File.CreateText(cacheFile).Close();

			this.CacheFileName = cacheFile;
		}

		/// <summary>
		/// The disk file where items are cached
		/// </summary>
		public string CacheFileName { get; private set; }

		public void WriteItem(T item)
		{
			var list = new List<T>();
			list.Add(item);
			WriteItem(list.ToArray());
		}

		/// <summary>
		/// Writes the item to the cache file
		/// </summary>
		/// <param name="item"></param>
		public void WriteItem(T[] list)
		{
			lock (locker)
			{
				const int MAXTRY = 3;
				var retry = 0;
				while (retry < MAXTRY)
				{
					try
					{
						//Open stream and move to end for writing
						using (var stream = File.Open(this.CacheFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite))
						{
							stream.Seek(0, SeekOrigin.End);
							var formatter = new BinaryFormatter();
							formatter.AssemblyFormat = System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Simple;
							formatter.TypeFormat = System.Runtime.Serialization.Formatters.FormatterTypeStyle.TypesWhenNeeded;
							formatter.Binder = new versionConfigToNamespaceAssemblyObjectBinder();
							foreach (var item in list)
							{
								formatter.Serialize(stream, item);
							}
							stream.Close(); //Force file flush
							this.EOF = true;
							return;
						}
					}
					catch (Exception ex)
					{
						retry++;
						if (retry >= MAXTRY) throw;
					}
				}
			}
		}

		public bool EOF { get; private set; }

		/// <summary>
		/// Gets the next object in the cache file
		/// </summary>
		/// <returns></returns>
		public T ReadItem()
		{
			try
			{
				if (!File.Exists(this.CacheFileName))
					throw new Exception("The cache file does not exist!");

				lock (locker)
				{
					using (var stream = File.Open(this.CacheFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite))
					{
						stream.Seek(_lastPosition, SeekOrigin.Begin);
						if (stream.Position == stream.Length)
						{
							this.EOF = true;
							return default(T);
						}
						else
						{
							var formatter = new BinaryFormatter();
							formatter.TypeFormat = System.Runtime.Serialization.Formatters.FormatterTypeStyle.TypesWhenNeeded;
							formatter.AssemblyFormat = System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Simple;
							formatter.Binder = new versionConfigToNamespaceAssemblyObjectBinder();
							var retval = (T)formatter.Deserialize(stream);
							_lastPosition = stream.Position;
							stream.Close();
							return retval;
						}
					}
				}

			}
			catch (Exception ex)
			{
				throw;
			}
		}

		/// <summary>
		/// Resets the file position to the begining
		/// </summary>
		public void ResetFile()
		{
			lock (locker)
			{
				this.EOF = false;
				_lastPosition = 0;
			}
		}

		public static List<T> LoadAll(string fileName)
		{
			//Read 20MB chunks and move forward when 90% processed (18MB)
			const int CHUNK = 1024 * 1024 * 20; //20 MB
			const int THRESHOLD = 1024 * 1024 * 2; //2 MB
			var retval = new List<T>();
			long lastPosition = 0;
			long absolutePosition = 0;
			if (File.Exists(fileName))
			{
				using (var fs = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite))
				{
					var array = new byte[CHUNK];
					var bytesRead = fs.Read(array, 0, CHUNK);
					using (var stream = new MemoryStream(array))
					{
						while (absolutePosition < fs.Length)
						{
							stream.Seek(lastPosition, SeekOrigin.Begin);
							if (stream.Position != stream.Length)
							{
								var formatter = new BinaryFormatter();
								var item = (T)formatter.Deserialize(stream);
								absolutePosition += (stream.Position - lastPosition);
								lastPosition = stream.Position;
								retval.Add(item);
							}

							if ((bytesRead == CHUNK) && ((CHUNK - stream.Position) < THRESHOLD))
							{
								fs.Seek(absolutePosition, SeekOrigin.Begin);
								var read = fs.Read(array, 0, CHUNK);
								if (read != 0)
								{
									//Something read
									stream.Position = 0;
									lastPosition = 0;
								}
							}
						}
					}
				}
			}
			return retval;
		}

		#region IDisposable Members

		void IDisposable.Dispose()
		{
			//try
			//{
			//  if (File.Exists(this.CacheFileName))
			//  {
			//    File.Delete(this.CacheFileName);
			//  }
			//}
			//catch (Exception ex)
			//{
			//  //Do Nothing
			//}
		}

		#endregion

		private sealed class versionConfigToNamespaceAssemblyObjectBinder : System.Runtime.Serialization.SerializationBinder
		{
			public override Type BindToType(string assemblyName, string typeName)
			{
				Type typeToDeserialize = null;
				try
				{
					var ToAssemblyName = assemblyName.Split(',')[0];
					var Assemblies = AppDomain.CurrentDomain.GetAssemblies();
					foreach (var ass in Assemblies)
					{
						if (ass.FullName.Split(',')[0] == ToAssemblyName)
						{
							typeToDeserialize = ass.GetType(typeName);
							break;
						}
					}
				}
				catch (System.Exception exception)
				{
					throw exception;
				}
				return typeToDeserialize;
			}
		}
	}
}